/*
 * datatandroid - Persistence framework for Android platform
 * 
 * Copyright (C) 2010 ProjectsException
 * <http://code.google.com/p/datandroid>
 * 
 * This file is part of datatandroid.
 * 
 * datatandroid is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or 
 * (at your option) any later version.
 * 
 * datatandroid is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with datatandroid.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.projectsexception.datandroid;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import android.content.ContentValues;
import android.content.Context;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.projectsexception.datandroid.database.AbstractCollectionProperty;
import com.projectsexception.datandroid.database.CollectionBasicProperty;
import com.projectsexception.datandroid.database.CollectionEntityProperty;
import com.projectsexception.datandroid.database.Database;
import com.projectsexception.datandroid.database.Entity;
import com.projectsexception.datandroid.database.EntityProperty;
import com.projectsexception.datandroid.database.IBasicProperty;
import com.projectsexception.datandroid.database.ICollectionProperty;
import com.projectsexception.datandroid.database.IProperty;
import com.projectsexception.datandroid.util.DatabaseUtil;
import com.projectsexception.datandroid.util.ReflectionUtil;

public class DatandroidImpl implements Datandroid {
    
    private DatandroidHelper dbHelper;
    private SQLiteDatabase db;
    private Database database;
    
    public void open(Context context, int resourceId) throws DatandroidException {
        synchronized (this) {
            if (dbHelper == null) {
                XmlResourceParser xml = context.getResources().getXml(resourceId);
                DatandroidSetup setup = new DatandroidSetup();
                database = setup.parseConfigFile(xml);
                dbHelper = new DatandroidHelper(context, database.getName(), database.getVersion(), 
                        DatabaseUtil.generateTables(database.getEntities().values()));
            }
            db = dbHelper.getWritableDatabase();
        }
    }

    public void close() {
        dbHelper.close();
    }
    
    public int getDatabaseVersion() {
        return database.getVersion();
    }
    
    public long insert(Object obj) throws DatandroidException {
        return save(obj, true, null, null);
    }
    
    public void insertAll(List<?> listObj) throws DatandroidException {
        saveAll(listObj, true);
    }
    
    public long update(Object obj) throws DatandroidException {
        return save(obj, false, null, null);
    }
    
    public void updateAll(List<?> listObj) throws DatandroidException {
        saveAll(listObj, false);
    }

    public void delete(Object obj) throws DatandroidException {
        delete(obj.getClass(), obj);
    }

    public void deleteAll(Class<?> objClass) throws DatandroidException {
        delete(objClass, null);
    }
        
    public <T> T get(Class<T> objClass, Long objId) throws DatandroidException {
        Entity entity = database.getEntities().get(objClass.getName());
        if (entity == null) {
            throw new DatandroidException("Class object " + objClass.getName() + " is not an declared entity");
        }
        List<T> result = get(objClass, new String[] {entity.getIdProperty().getColumnName()}, objId);
        if (result != null && result.size() > 0) {
            return result.get(0);
        } else {
            return null;
        }
    }
    
    public <T> List<T> getAll(Class<T> objClass) throws DatandroidException {
        return get(objClass, null, new Object[0]);
    }
    
    public <T> List<T> get(Class<T> objClass, String[] fields, Object... fieldValues) throws DatandroidException {
        Entity entity = database.getEntities().get(objClass.getName());
        if (entity == null) {
            throw new DatandroidException("Class object " + objClass.getName() + " is not an declared entity");
        }
        String[] selectionArgs = null;
        String selection = null;
        if (fields != null && fieldValues != null) {
            selectionArgs = new String[fieldValues.length];
            StringBuilder sb = new StringBuilder();
            if (fields.length != fieldValues.length) {
                throw new DatandroidException("The number of fields and field values are not the same");
            }
            Entity tmpEntity;
            int pos = 0;
            for (Object value : fieldValues) {
                if (java.util.Collection.class.isAssignableFrom(value.getClass())) {
                    throw new DatandroidException("Collections not supported for field values");
                }
                if (pos > 0) {
                    sb.append(" AND ");                    
                }
                sb.append(fields[pos]);
                sb.append("=?");
                if (DatabaseUtil.isBasicType(value.getClass())) {
                    selectionArgs[pos] = DatabaseUtil.getDatabaseValue(value);
                } else {
                    tmpEntity = database.getEntities().get(value.getClass().getName());
                    if (tmpEntity == null) {
                        throw new DatandroidException("Class field value " + value.getClass().getName() + " is not an declared entity");
                    }
                    Object tmpId = ReflectionUtil.executeGetter(value, tmpEntity.getIdProperty().getName());
                    Long subObjId = ReflectionUtil.castNumber(tmpId, Long.class);
                    if (subObjId == null || subObjId == 0) {
                        throw new DatandroidException("Field value " + value.getClass().getName() + " is not a saved entity");
                    }
                    selectionArgs[pos] = subObjId.toString();
                }
                pos++;
            }
            selection = sb.toString();
        }
        return query(objClass, selection, selectionArgs);
    }
    
    @SuppressWarnings("unchecked")
    private <T> List<T> query(Class<T> objClass, String selection, String[] selectionArgs) throws DatandroidException {
        Entity entity = database.getEntities().get(objClass.getName());
        if (entity == null) {
            throw new DatandroidException("Class object " + objClass.getName() + " is not an declared entity");
        }
        List<T> resultList = new ArrayList<T>();
        List<AbstractCollectionProperty> colProps = new ArrayList<AbstractCollectionProperty>();
        List<EntityProperty> entityProps = new ArrayList<EntityProperty>();
        List<IBasicProperty> basicProps = new ArrayList<IBasicProperty>();
        List<String> queryColumns = new ArrayList<String>();
        List<String> entityColumns = new ArrayList<String>();
        queryColumns.add(entity.getIdProperty().getColumnName());
        for (IProperty prop : entity.getProperties()) {
            if (prop instanceof AbstractCollectionProperty) {
                colProps.add((AbstractCollectionProperty) prop);
            } else if (prop instanceof EntityProperty) {
                EntityProperty entityProp = (EntityProperty) prop;
                entityColumns.add(entityProp.getColumnName());
                entityProps.add(entityProp);                
            } else {
                IBasicProperty basicProp = (IBasicProperty) prop;
                queryColumns.add(basicProp.getColumnName());
                basicProps.add(basicProp);
            }
        }
        queryColumns.addAll(entityColumns);
        Cursor c = db.query(entity.getTableName(), queryColumns.toArray(new String[0]), selection, selectionArgs, null, null, null); 
        if (c != null) {
            while (c.moveToNext()) {
                T result;
                try {
                    result = objClass.newInstance();
                } catch (Exception e) {
                    throw new DatandroidException(e.getLocalizedMessage());
                }
                Long id = c.getLong(0);
                ReflectionUtil.executeSetter(result, entity.getIdProperty().getName(), id);
                int pos = 1;
                for (IBasicProperty prop : basicProps) {
                    Object value = DatabaseUtil.getColumnValue(c, pos++, prop.getClassName());
                    ReflectionUtil.executeSetter(result, prop.getName(), value);
                }
                for (EntityProperty prop : entityProps) {
                    Long subObjId = c.getLong(pos++);
                    Object value = get(ReflectionUtil.getClassForName(prop.getClassName()), subObjId);
                    ReflectionUtil.executeSetter(result, prop.getName(), value);
                }
                for (ICollectionProperty prop : colProps) {
                    Collection values;
                    if (prop instanceof CollectionBasicProperty) {
                        CollectionBasicProperty colProp = (CollectionBasicProperty) prop;
                        values = new ArrayList();
                        String[] columns = {"VALUE"};
                        Cursor subC = db.query(colProp.getTableName(), columns, 
                                entity.getTableName() + "_ID = ?", new String[]{id.toString()}, 
                                null, null, null);
                        if (subC != null) {
                            while(subC.moveToNext()) {
                                if (DatabaseUtil.isBasicType(prop.getClassItem())) {
                                    values.add(DatabaseUtil.getColumnValue(subC, 0, prop.getClassItem()));
                                }
                            }
                            subC.close();
                        }
                    } else {
                        // Recuperamos todos los objetos del tipo de colección cuyo id padre sea el recuperado
                        CollectionEntityProperty colProp = (CollectionEntityProperty) prop;
                        Class<?> classItem = ReflectionUtil.getClassForName(colProp.getClassItem());
                        values = query(classItem, colProp.getColumnName() + " = ?", new String[] {id.toString()});
                    }
                    ReflectionUtil.executeSetter(result, prop.getName(), values);
                }
                resultList.add(result);
            }
            c.close();
        }        
        return resultList;
    }
    
    public SQLiteDatabase getSQLiteDatabase() {
        return db;
    }

    @SuppressWarnings("unchecked")
    private long save(Object obj, boolean newObject, String[] columns, String[] columnValues) throws DatandroidException {
        Entity entity = database.getEntities().get(obj.getClass().getName());
        if (entity == null) {
            throw new DatandroidException("Class object " + obj.getClass().getName() + " is not an declared entity");
        }
        Long objId = null;
        if (!newObject) {
            Object idResult = ReflectionUtil.executeGetter(obj, entity.getIdProperty().getName());
            objId = ReflectionUtil.castNumber(idResult, Long.class);
            if (objId == null) {
                throw new DatandroidException("Object " + obj + " is not an saved entity");
            }
        }
        List<IProperty> props = new ArrayList<IProperty>();
        ContentValues values = new ContentValues();
        Object value;
        for (IProperty prop : entity.getProperties()) {
            value = ReflectionUtil.executeGetter(obj, prop.getName());
            if (prop instanceof ICollectionProperty) {
                // Se trata de una propiedad de colección, la guardamos para insertarla luego
                // cuando tengamos el ID del padre
                props.add(prop);
            } else if (prop instanceof EntityProperty) {
                // Propiedad entidad, la guardamos y luego insertamos la clave en 
                // la tabla de la entidad padre
                EntityProperty entityProp = (EntityProperty) prop;
//                value = executeGetter(obj, prop.getName());
                Long subObjId = null;
                
//                if (newObject) {
                    Entity subEntity = database.getEntities().get(value.getClass().getName());
                    if (subEntity == null) {
                        throw new DatandroidException("Class object " + value.getClass().getName() + " is not an declared entity");
                    }
                    Object tmpId = ReflectionUtil.executeGetter(value, entity.getIdProperty().getName());
                    subObjId = ReflectionUtil.castNumber(tmpId, Long.class);
//                }
                if (subObjId == null || subObjId == 0) {
                    subObjId = insert(value);
                } else {
                    update(value);
                }                    
                
                values.put(entityProp.getColumnName(), subObjId);
            } else {
                // Es una propiedad básica
                IBasicProperty basicProp = (IBasicProperty) prop;
                values.put(basicProp.getColumnName(), DatabaseUtil.getDatabaseValue(value));
            }
        }
        if (columns != null && columnValues != null && columns.length == columnValues.length) {
            for (int i = 0; i < columns.length; i++) {
                values.put(columns[i], columnValues[i]);
            }
        }
        if (newObject) {
            objId = db.insert(entity.getTableName(), null, values);
        } else {
            db.update(entity.getTableName(), values, entity.getIdProperty().getColumnName() + "=?", new String[]{objId.toString()});
        }        
        // Ahora que tenemos el id del padre pasamos con las propiedades de colección
        for (IProperty prop : props) {
            Collection col = (Collection) ReflectionUtil.executeGetter(obj, prop.getName());
            if (prop instanceof CollectionBasicProperty) {
                CollectionBasicProperty colProp = (CollectionBasicProperty) prop;
                if (!newObject) {             
                    // Si es un update, primero eliminamos los valores anteriores
                    db.delete(colProp.getTableName(), entity.getTableName() + "_ID", new String[]{objId.toString()});
                }              
                // Es un tipo básico, luego guardamos los valores en la tabla
                for (Object objItem : col) {
                    values.clear();
                    values.put(entity.getTableName() + "_ID", objId);
                    values.put("VALUE", DatabaseUtil.getDatabaseValue(objItem));
                    db.insert(colProp.getTableName(), null, values);
                }
            } else {
                CollectionEntityProperty colProp = (CollectionEntityProperty) prop;
                Long objIdItem = null;
                Entity itemEntity = database.getEntities().get(colProp.getClassItem());
                for (Object objItem : col) {
                    if (!newObject) {
                        // Hay que ver en la colección qué entidades ya están insertadas
                        Object idResult = ReflectionUtil.executeGetter(objItem, itemEntity.getIdProperty().getName());
                        objIdItem = ReflectionUtil.castNumber(idResult, Long.class);
                        // objIdItem == null -> Nuevo objeto
                    }
                    save(objItem, objIdItem == null, new String[] {colProp.getColumnName()}, new String[] {objId.toString()});                        
                }
            }
        }
        ReflectionUtil.executeSetter(obj, entity.getIdProperty().getName(), objId);
        return objId;
    }
    
    private void saveAll(List<?> listObj, boolean newObject) throws DatandroidException {
        for (Object obj : listObj) {
            save(obj, newObject, null, null);
        }
    }

    private void delete(Class<?> objClass, Object obj) throws DatandroidException {
        String className = objClass.getName();
        Entity entity = database.getEntities().get(className);
        if (entity == null) {
            throw new DatandroidException("Class object " + className + " is not an declared entity");
        }
        Long objId = null;
        if (obj != null) {
            // Queremos eliminar un objeto en concreto
            Object idResult = ReflectionUtil.executeGetter(obj, entity.getIdProperty().getName());
            objId = ReflectionUtil.castNumber(idResult, Long.class);
            if (objId == null) {
                throw new DatandroidException("Object " + obj + " is not an saved entity");
            }
        }
        if (entity.getProperties() != null) {
            for (IProperty prop : entity.getProperties()) {
                if (prop instanceof EntityProperty) {
                    // Tenemos que ver si la propiedad se elimina en cascada
                    if (((EntityProperty) prop).isDeleteCascade()) {
                        if (obj != null) {
                            Object subObj = ReflectionUtil.executeGetter(obj, prop.getName());
                            delete(subObj);
                        } else {
                            // Obtenemos la lista de id's que pertenecian a esta entidad
                            Cursor cEntity = db.query(entity.getTableName(), 
                                    new String[] {((EntityProperty) prop).getColumnName()}, 
                                    null, null, null, null, null);
                            if (cEntity != null) {
                                Entity subEntity;
                                while(cEntity.moveToNext()) {
                                    subEntity = database.getEntities().get(prop.getClassName());
                                    db.delete(subEntity.getTableName(), 
                                            subEntity.getIdProperty().getColumnName() + " = ?", 
                                            new String[]{cEntity.getString(0)});
                                }
                                cEntity.close();
                            }
                        }
                    }
                }
                if (prop instanceof ICollectionProperty) {
                    if (prop instanceof CollectionBasicProperty) {
                        // Si la colección es de tipo básico eliminamos toda la tabla
                        CollectionBasicProperty colProp = (CollectionBasicProperty) prop;
                        db.delete(colProp.getTableName(), null, null);
                    } else {
                        // Si la colleción es de tipo entidad, eliminamos las filas cuyo
                        // id del padre sea el que estamos eliminando o si estamos eliminando
                        // todas las instancias, entonces la condición será que el id del padre
                        // sea distinto de null
                        CollectionEntityProperty colProp = (CollectionEntityProperty) prop;
                        Entity itemEntity = database.getEntities().get(colProp.getClassItem());
                        String selection;
                        String[] selectionArgs = null;
                        if (objId == null) {
                            // Todas las instancias
                            selection = colProp.getColumnName() + " IS NOT NULL";
                        } else {
                            selection = colProp.getColumnName() + " = ?";
                            selectionArgs = new String[]{objId.toString()};
                        }
                        db.delete(itemEntity.getTableName(), selection, selectionArgs);
                    }                    
                }
            }            
        }
        String selection = null;
        String[] selectionArgs = null;
        if (obj != null) {
            selection = entity.getIdProperty().getColumnName() + "=?";
            selectionArgs = new String[]{objId.toString()};
        }
        db.delete(entity.getTableName(), selection, selectionArgs);
    }

}
